# -*- coding: UTF-8 -*-
import errno
import os
import json
import select
import socket
import threading
import traceback
from SocketServer import DatagramRequestHandler

from addon_executor import ExecutorWrapper
from addon_converter import AddOnConverter
from addon_manager.addon_exception import AddOnBaseException, NoSuchAddOnCtrlCmd

from addon_manager.addon_exception_code import RES_SUCC, RES_FAIL
from addon_manager.addon_grpc_def import (
    CMD_INSTALL_ASYNC,
    CMD_PACK_ADDON_ASYNC,
    CMD_START,
    CMD_STOP,
    CMD_STOPALL,
    CMD_GET_ALIVE_STATUS,
    CMD_INSTALL,
    CMD_GET_ADDON_LIST,
    CMD_DELETE_ADDON,
    CMD_PACK_ADDON,
    CMD_GET_INSTALL_STATUS,
    ADDON_CMD_CH,
    CMD_CONVERT_ADDON,
)

from addon_manager.addon_config import AddOnConfig
from addon_manager.addon_installer import AddOnAsyncInstaller, AddonSyncInstaller
from addon_manager.addon_logger import AddOnLogger
from addon_manager.addon_status import AddOnStatus
from addon_manager.addon_eventqueue import AddOnEventID, AddOnEventQueue
from addon_manager.addon_whitelist import AddOnWhitelist


class AddOnController(object):

    __INST = None

    @staticmethod
    def inst():
        if AddOnController.__INST is None:
            AddOnController.__INST = AddOnController()
        return AddOnController.__INST

    @staticmethod
    def release():
        AddOnController.__INST.addon_executor.stop_all()
        AddOnController.__INST = None

    def __init__(self):
        try:
            object.__init__(self)
            self.addon_config = AddOnConfig.inst()
            self.addon_status = AddOnStatus.inst()
            self.addon_whitelist = AddOnWhitelist.inst()
            self.addon_executor = ExecutorWrapper(config=self.addon_config)
            self.addon_installer_async = AddOnAsyncInstaller(config=self.addon_config)
            self.addon_installer_sync = AddonSyncInstaller(config=self.addon_config)
        except Exception:
            AddOnLogger().error(traceback.format_exc())

    def start(self, param):
        # print('addon_controller start({})'.format(param))
        service_path = param["service_path"]
        self.addon_executor.start(service_path)
        return True, None

    def stop(self, param):
        # print('addon_controller stop({})'.format(param))
        service_path = param["service_path"]
        self.addon_executor.stop(service_path)
        return True, None

    def stop_all(self, _param):
        # print('addon_controller stop_all({})'.format(param))
        self.addon_executor.stop_all()
        return True, None

    def get_alive_status(self, _param):
        # print('addon_controller get_alive_status({})'.format(_param))
        self.addon_executor.update_status()
        status = self.addon_executor.get_alive_status()
        return True, status

    def install(self, param):
        ftppath = param["ftppath"]
        self.addon_executor.update_status()
        addoninfo = self.addon_installer_sync.install(ftppath=ftppath)
        return True, addoninfo

    def get_addon_list(self, _param):
        # print('addon_controller get_addon_list({})'.format(param))
        addonlist = self.addon_status.get_addon_list()
        return True, addonlist

    def delete_addon(self, param):
        service_path = param["service_path"]
        self.addon_executor.update_status()
        self.addon_installer_sync.uninstall(service_path)
        return True, None

    def pack_addon(self, param):
        filename = param["filename"]
        self.addon_executor.update_status()
        self.addon_installer_sync.pack_addon(filename)
        return True, None

    def get_install_status(self, _param):
        install_status = self.addon_installer_async.get_install_status()
        return True, install_status

    def install_async(self, param):
        ftppath = param["ftppath"]
        self.addon_executor.update_status()
        addoninfo = self.addon_installer_async.install(ftppath=ftppath)
        return True, addoninfo

    def pack_addon_async(self, param):
        filename = param["filename"]
        self.addon_executor.update_status()
        self.addon_installer_async.pack_addon(filename)
        return True, None

    def on_install_finish(self):
        self.addon_installer_async.on_install_async_finish()

    def on_pack_finish(self):
        self.addon_installer_async.on_pack_async_finish()

    def convert_addon(self, _param):
        AddOnConverter.run()
        return True, None


class AddOnRequestHandler(DatagramRequestHandler):

    @staticmethod
    def class_init():
        __ADDON_CONTROLLER = AddOnController.inst()

        AddOnRequestHandler.__FUNC_MAP = {
            CMD_START: __ADDON_CONTROLLER.start,
            CMD_STOP: __ADDON_CONTROLLER.stop,
            CMD_STOPALL: __ADDON_CONTROLLER.stop_all,
            CMD_GET_ALIVE_STATUS: __ADDON_CONTROLLER.get_alive_status,
            CMD_INSTALL: __ADDON_CONTROLLER.install,
            CMD_GET_ADDON_LIST: __ADDON_CONTROLLER.get_addon_list,
            CMD_DELETE_ADDON: __ADDON_CONTROLLER.delete_addon,
            CMD_PACK_ADDON: __ADDON_CONTROLLER.pack_addon,
            CMD_GET_INSTALL_STATUS: __ADDON_CONTROLLER.get_install_status,
            CMD_INSTALL_ASYNC: __ADDON_CONTROLLER.install_async,
            CMD_PACK_ADDON_ASYNC: __ADDON_CONTROLLER.pack_addon_async,
            CMD_CONVERT_ADDON: __ADDON_CONTROLLER.convert_addon,
        }

        AddOnRequestHandler.__CMD_MAP_STR = {
            CMD_START: "CMD_START",
            CMD_STOP: "CMD_STOP",
            CMD_STOPALL: "CMD_STOPALL",
            CMD_GET_ALIVE_STATUS: "CMD_GET_ALIVE_STATUS",
            CMD_INSTALL: "CMD_INSTALL",
            CMD_GET_ADDON_LIST: "CMD_GET_ADDON_LIST",
            CMD_DELETE_ADDON: "CMD_DELETE_ADDON",
            CMD_PACK_ADDON: "CMD_PACK_ADDON",
            CMD_GET_INSTALL_STATUS: "CMD_GET_INSTALL_STATUS",
            CMD_INSTALL_ASYNC: "CMD_INSTALL_ASYNC",
            CMD_PACK_ADDON_ASYNC: "CMD_PACK_ADDON_ASYNC",
            CMD_CONVERT_ADDON: "CMD_CONVERT_ADDON",
        }

    @staticmethod
    def class_deinit():
        AddOnController.release()

    def __parse_request(self, req_str):
        req = json.loads(req_str)
        cmd = req["cmd"]
        param = req["param"]
        return cmd, param

    def __dispatch(self, cmd, param):
        """
        此处需要确认__FUNC_MAP中的函数返回值ret_data可以转换为json
        """
        try:
            func = self.__FUNC_MAP[cmd]
        except KeyError:
            raise NoSuchAddOnCtrlCmd(cmd)

        res, ret_data = self.__FUNC_MAP[cmd](param)
        ret_code = RES_SUCC if res else RES_FAIL
        response = {}
        response["cmd"] = cmd
        response["code"] = ret_code
        response["data"] = ret_data
        response["msg"] = ""
        return response

    def __handle_unexpect_exception(self, cmd=None):
        response = {}
        response["cmd"] = cmd
        response["code"] = RES_FAIL
        response["data"] = None
        response["msg"] = "Unexpected Failure"
        return response

    def __handle_addon_base_exception(self, e, cmd=None):
        response = {}
        response["cmd"] = cmd
        response["code"] = e.code()
        response["data"] = None
        response["msg"] = e.msg()
        return response

    def __cmd_to_str(self, cmd):
        cmd_str = self.__CMD_MAP_STR.get(cmd)
        cmd_str = cmd_str if cmd_str is not None else "UNKNOWN CMD"
        return cmd_str

    def __res_dict_to_log(self, res):
        try:
            cmd_str = self.__cmd_to_str(res["cmd"])
            code_str = str(res["code"])
            data_str = str(res["data"])
            msg_str = str(res["msg"])
            return "{cmd: %s, code: %s, msg: %s, data %s}" % (cmd_str, code_str, data_str, msg_str)
        except Exception:
            return None

    def __req_dict_to_log(self, cmd, param):
        try:
            cmd_str = self.__cmd_to_str(cmd)
            param_str = str(param)
            return "{cmd: %s, param: %s}" % (cmd_str, param_str)
        except Exception:
            return None

    def __send_json_res(self, res):
        if res is None:
            res = self.__handle_unexpect_exception()
        res_str = json.dumps(res)
        log_str = self.__res_dict_to_log(res)
        if log_str is not None:
            AddOnLogger().debug("send reponse str: %s", log_str)
        else:
            AddOnLogger().debug("send reponse str: %s", res_str)
        self.wfile.write(res_str)

    def handle(self):
        request = self.rfile.getvalue()
        cmd = None
        res = None

        try:
            cmd, param = self.__parse_request(request)
            log_req_str = self.__req_dict_to_log(cmd, param)
            if log_req_str is None:
                AddOnLogger().debug("recv request: %s", request)
            else:
                AddOnLogger().debug("recv request: %s", log_req_str)
        except Exception:
            AddOnLogger().error(traceback.format_exc())
            res = self.__handle_unexpect_exception(cmd)
            self.__send_json_res(res)
            return

        try:
            res = self.__dispatch(cmd, param)
        except AddOnBaseException as e:
            AddOnLogger().error(traceback.format_exc())
            AddOnLogger().error("{}[{}]".format(e.msg(), hex(e.code())))
            res = self.__handle_addon_base_exception(e, cmd)
            self.__send_json_res(res)
            return
        except Exception:
            AddOnLogger().error(traceback.format_exc())
            res = self.__handle_unexpect_exception(cmd)
            self.__send_json_res(res)
            return

        self.__send_json_res(res)


class AddOnEventHandler:

    @staticmethod
    def class_init():
        __ADDON_CONTROLLER = AddOnController.inst()

        AddOnEventHandler.__FUNC_MAP = {
            AddOnEventID.EVENT_ON_INSTALL_FINISH: __ADDON_CONTROLLER.on_install_finish,
            AddOnEventID.EVENT_ON_PACK_FINISH: __ADDON_CONTROLLER.on_pack_finish,
        }

    def __init__(self, eventid):
        self.eventid = eventid
        self.handle()

    def handle(self):
        try:
            self.__FUNC_MAP[self.eventid]()
        except Exception:
            AddOnLogger().error(traceback.format_exc())


def _eintr_retry(func, *args):
    """restart a system call interrupted by EINTR"""
    while True:
        try:
            return func(*args)
        except (OSError, select.error) as e:
            if e.args[0] != errno.EINTR:
                raise


def _init_addr(addr):
    sock_ch_dir = os.path.dirname(addr)
    if not os.path.exists(sock_ch_dir):
        os.mkdir(sock_ch_dir)

    if os.path.exists(addr):
        os.unlink(addr)


class AddOnBaseServer(object):

    __ADDRESS_FAMILY = socket.AF_UNIX
    __SOCKET_TYPE = socket.SOCK_DGRAM
    __MAX_PACKET_SIZE = 8192

    @staticmethod
    def class_init():
        AddOnBaseServer.__ADDON_EVENT_QUEUE = AddOnEventQueue.inst()

    def __init__(self, cmd_addr, cmd_handler_class, event_handler_class):
        self.__cmd_addr = cmd_addr
        self.__is_shut_down = threading.Event()
        self.__shutdown_request = False
        self.__cmd_sock = self.__init_sock(self.__cmd_addr)
        self.__handler_class_dict = {self.__cmd_sock: cmd_handler_class}
        self.__event_handler_class = event_handler_class

    def __init_sock(self, addr):
        sock = socket.socket(self.__ADDRESS_FAMILY, self.__SOCKET_TYPE)
        sock.bind(addr)
        return sock

    def serve_forever(self, poll_interval=0.5):
        """Handle one request at a time until shutdown.

        Polls for shutdown every poll_interval seconds. Ignores
        self.timeout. If you need to do periodic tasks, do them in
        another thread.
        """
        self.__is_shut_down.clear()
        try:
            while not self.__shutdown_request:
                r, _w, _e = _eintr_retry(select.select, [self.__cmd_sock], [], [], poll_interval)

                for item in self.__handler_class_dict.items():
                    if item[0] in r:
                        self._handle_request_noblock(item)

                self._handle_event_nonblock()

        finally:
            self.__shutdown_request = False
            self.__is_shut_down.set()

    def _handle_request_noblock(self, sock_handler_pair):
        """Handle one request, without blocking.

        I assume that select.select has returned that the socket is
        readable before this function was called, so there should be
        no risk of blocking in get_request().
        """
        try:
            server_sock, _handler_class = sock_handler_pair
            request, client_address = self.get_request(server_sock)
        except socket.error:
            return
        try:
            self.process_request(request, client_address, sock_handler_pair)
        except Exception:
            self.handle_error(request, client_address, sock_handler_pair)

    def _handle_event_nonblock(self):
        try:
            while not self.__ADDON_EVENT_QUEUE.queue.empty():
                eventid = self.__ADDON_EVENT_QUEUE.queue.get(False)
                self.process_event(eventid)
        except Exception:
            AddOnLogger().error(traceback.format_exc())

    def shutdown(self):
        """Stops the serve_forever loop.

        Blocks until the loop has finished. This must be called while
        serve_forever() is running in another thread, or it will
        deadlock.
        """
        self.__shutdown_request = True
        self.__is_shut_down.wait()

    def get_request(self, serversock):
        data, client_addr = serversock.recvfrom(self.__MAX_PACKET_SIZE)
        return (data, serversock), client_addr

    def handle_error(self, _request, client_address, _sock_handler_pair):
        AddOnLogger().error("failed to handle request %s", str(client_address))
        AddOnLogger().error(traceback.format_exc())

    def process_request(self, request, client_address, sock_handler_pair):
        self.finish_request(request, client_address, sock_handler_pair)

    def finish_request(self, request, client_address, sock_handler_pair):
        _server_sock, handler_class = sock_handler_pair
        handler_class(request, client_address, self)

    def process_event(self, eventid):
        self.__event_handler_class(eventid)


def addon_controller_start():
    AddOnRequestHandler.class_init()
    AddOnEventHandler.class_init()
    AddOnBaseServer.class_init()
    try:
        cmd_server = AddOnBaseServer(ADDON_CMD_CH, AddOnRequestHandler, AddOnEventHandler)
        cmd_server.serve_forever()
    finally:
        AddOnRequestHandler.class_deinit()


__all__ = ["addon_controller_start"]
